package com.notetool.util.tokenparser;

import java.util.ArrayList;
import java.util.HashMap;

import com.notetool.util.tokenparser.semantic.ParserSemanticException;
import com.notetool.util.tokenparser.semantic.ParsingRestriction;
import com.notetool.util.tokenparser.semantic.TokenRestriction;
import com.notetool.util.tokenparser.syntax.Model;
import com.notetool.util.tokenparser.syntax.ParserSyntaxException;
import com.notetool.util.tokenparser.syntax.SingleCharModel;
import com.notetool.util.tokenparser.syntax.WordModel;

public class Parser
{
    
    ////////////////////
    // Members
    HashMap<Character, SingleCharModel> singleChars;
    ArrayList<WordModel> words;
    ArrayList<ParsingRestriction> restrictions;
    
    ////////////////////
    // Constructors
    public Parser ()
    {
        singleChars = new HashMap<Character, SingleCharModel> ();
        words = new ArrayList<WordModel> ();
        restrictions = new ArrayList<ParsingRestriction> ();
    }
    
    ////////////////////
    // Accessors
    
    ////////////////////
    // Public methods
    public Parser addSingleCharModel (SingleCharModel... singleCharModels)
    {
        for (SingleCharModel model : singleCharModels)
            if (model != null)
               singleChars.put(model.getCharacter(), model);
        
        return this;
    }
    
    public Parser addWordModel (WordModel... wrds)
    {
        for (WordModel word : wrds)
            if (word != null)
                if (!words.contains (word))
                    words.add (word);
        
        return this;
    }
    
    public TokenList getTokens (String expression, IParserMonitor monitor) throws ParserException
    {
        TokenList tokens = tokenize (expression, monitor);
        validateRules (tokens, monitor);
        
        return tokens;
    }
    
    public Parser addRestriction (ParsingRestriction restriction)
    {
        if (restriction == null)
            return this;
        
        if (!restrictions.contains (restriction))
            restrictions.add (restriction);
        
        return this;
    }
    
    ////////////////////
    // Private methods
    private TokenList tokenize (String expression, IParserMonitor monitor) throws ParserSyntaxException, ParserStoppedException
    {
        TokenList tokens = new TokenList ();
        
        if (expression == null)
            return tokens;
        
        // Parse tokens until we're done
        ParsingResult result;
        ArrayList<WordModel> preferedNext = new ArrayList<WordModel> ();
        while ((result = parseFirstToken (expression, preferedNext)) != null)
        {
            // Get the first token in the string
            Token token = result.getToken ();
            tokens.add (token);
            
            // Prepare for next pass
            expression = result.getRemainder ();
            preferedNext.addAll (0, token.getModel ().getPreferedNext ());
            
            // Run checkpoint
            if (monitor != null)
                monitor.parserCheckpoint ();
        }
        
        return tokens;
    }
    
    private void validateRules (TokenList list, IParserMonitor monitor) throws ParserSemanticException, ParserStoppedException
    {
        if (list == null)
            throw new NullPointerException ();
        
        // Validate TokenRestriction's
        Token curToken = list.getFirst ();
        while (curToken != null)
        {
            // Validate semantics for the current token
            curToken.validateSemantics ();
            
            // Run checkpoint
            if (monitor != null)
                monitor.parserCheckpoint ();
            
            // Next token
            curToken = curToken.getNext ();
        }
        
        // Validate ParsingRestriction's
        for (ParsingRestriction restriction : restrictions)
            restriction.checkRestriction (list);
    }
    
    private ParsingResult parseFirstToken (String expression, ArrayList<WordModel> preferedNext) throws ParserSyntaxException
    {
        
        // Check empty null/empty String
        if (expression == null || expression.isEmpty ())
            return null;
        
        char firstChar = expression.charAt (0);
        
        // Retrieve SingleCharModel for first char is possible
        SingleCharModel model = singleChars.get(firstChar);
        
        // If we do have a SingleCharModel, we instantiate the token and return
        if (model != null)
        {
            SingleCharToken token = model.instantiate ();
            String remainder = expression.substring (1);
            return new ParsingResult (token, remainder);
        }
        
        // If we make it here, we're about to parse a word
        StringBuilder buffer = new StringBuilder ("");
        
        // Check if character is a quote
        boolean inQuote = (firstChar == '"');
        
        // If not a quote, we want to grab the character as a part of the word
        if (!inQuote)
            buffer.append(firstChar);
        
        // Iterate on the next characters till the end of the expression, or until we meet a SingleCharToken for a normal String, and a " for a quoted String
        int charPos;
        boolean inEscape = false;
        for (charPos = 1; charPos < expression.length (); ++ charPos)
        {
            char curChar = expression.charAt (charPos);
            
            // Handle quoted string parsing
            if (inQuote)
            {
                if (inEscape)
                {
                    switch (curChar)
                    {
                        case '"':
                            buffer.append ('\"');
                            break;
                        case '\\':
                            buffer.append ('\\');
                            break;
                        default:
                            throw new ParserSyntaxException ("Could not parse unknown escape sequence.");
                    }
                    
                    inEscape = false;
                    continue;
                }
                
                if (curChar == '\\')
                {
                    inEscape = true;
                    continue;
                }
                
                // If we have met the matching quote, stop String parsing
                if (curChar == '\"')
                {
                    inQuote = false;
                    charPos += 1; // We don't want to have the closing " in the remainder
                    break;
                }
                
                // If we've made it here, the character we're currently on is part of the literal string, continue parsing
                buffer.append (curChar);
                continue;
            }
            
            // We're not parsing a quoted string, so if we meet a SingleCharToken we're done parsing
            SingleCharModel curModel = singleChars.get(curChar);
            if (curModel != null)
                break;
            
            // The quote character breaks a non quoted string
            if (curChar == '\"')
                break;
            
            // If we make it here, add the character to the buffer
            buffer.append (curChar);
        }
        
        // We're done parsing the string/have reached the end. Check if we have unfinished business
        if (inQuote || inEscape)
            throw new ParserSyntaxException ("End of expression reached unexpectedly.");
        
        String word = buffer.toString ();
        
        // See if we can match the word with any of the prefered nexts
        WordModel matchedModel = findWordModelMatch (word, preferedNext);
        
        // If not, then go for the global WordModel list
        if (matchedModel == null)
            matchedModel = findWordModelMatch (word, words);
        
        // Clear the prefered next models list
        preferedNext.clear ();
        
        // Create result if model found
        if (matchedModel != null)
        {
            // Grab the remainder
            String remainder = "";
            if (charPos <= (expression.length () - 1))
                remainder = expression.substring (charPos);
        
            // Return result
            return new ParsingResult (matchedModel.instantiate (word), remainder);
        }
        
        // If we reach this point, we couldn't parse the WordToken...
        throw new ParserSyntaxException ("Could not parse word.");
    }
    
    private WordModel findWordModelMatch (String word, ArrayList<WordModel> models)
    {
        for (WordModel currentModel : models)
        {
            if (currentModel.matches (word))
            {
                return currentModel;
            }
        }
        
        return null;
    }
    

    
}
